/******************************************************************************
 * Copyright (c) 2002, 2023 IBM Corporation and others.
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    IBM Corporation - initial API and implementation 
 ****************************************************************************/

package org.eclipse.gmf.tests.runtime.diagram.ui.label;

import java.lang.ref.WeakReference;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.WeakHashMap;

import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.FigureUtilities;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.LayoutManager;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gmf.runtime.draw2d.ui.internal.mapmode.IMapModeHolder;
import org.eclipse.gmf.runtime.draw2d.ui.mapmode.IMapMode;
import org.eclipse.gmf.runtime.draw2d.ui.mapmode.MapModeUtil;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.Image;


/**
 * An extended label that has the following extra features:
 * 
 * 1- It is capable of showing selection and focus feedback (primary or
 * secondary) 2- It is capable of optionally underlining the label's text 3- It
 * is capable of wrapping the label's text at a given width with a given
 * alignment 4- It is capable of supporting multiple label icons (temporary
 * feature)
 * 
 * This class was originally deriving off Draw2d's <code>Label</code> class
 * but with the introduction of the auto-wrapping feature, a copy had to be made
 * overriding was not straightforward. Hopefully, this extended version can be
 * pushed to opensource
 * 
 * <p>
 * Code taken from Eclipse reference bugzilla #98820
 * 
 * @author melaasar
 */
public class OriginalWrapLabel
	extends Figure
	implements PositionConstants {	

	private static final String _ellipse = "..."; //$NON-NLS-1$

	private static final Dimension EMPTY_DIMENSION = new Dimension(0, 0);

	private static final Map mapModeConstantsMap = new WeakHashMap();

	private static class MapModeConstants {

		private static final int MAX_IMAGE_INFO = 12;

		public final WeakReference mapModeRef;

		public final int nDPtoLP_3;

		public final int nDPtoLP_2;

		public final int nDPtoLP_0;

		public final Dimension dimension_nDPtoLP_0;

		public final WeakHashMap fontToEllipseTextSize = new WeakHashMap();

		public final SingleIconInfo[] singleIconInfos = new SingleIconInfo[MAX_IMAGE_INFO];

		public MapModeConstants(IMapMode mapMode) {
			this.mapModeRef = new WeakReference(mapMode);
			nDPtoLP_2 = mapMode.DPtoLP(2);
			nDPtoLP_3 = mapMode.DPtoLP(3);
			nDPtoLP_0 = mapMode.DPtoLP(0);
			dimension_nDPtoLP_0 = new Dimension(nDPtoLP_0, nDPtoLP_0);
		}

		public Dimension getEllipseTextSize(Font f) {
			Dimension d = (Dimension) fontToEllipseTextSize.get(f);
			if (d == null) {
				IMapMode mapMode = (IMapMode) mapModeRef.get();
				d = FigureUtilities.getTextExtents(_ellipse, f);
				d.height = FigureUtilities.getFontMetrics(f).getHeight();
				d = new Dimension(mapMode.DPtoLP(d.width), mapMode
					.DPtoLP(d.height));
				fontToEllipseTextSize.put(f, d);
			}
			return d;
		}

		public SingleIconInfo getSingleIconInfo(Image image) {
			if (image == null) {
				return SingleIconInfo.NULL_INFO;
			}
			SingleIconInfo info;
			for (int i = 0; i < MAX_IMAGE_INFO; ++i) {
				info = singleIconInfos[i];
				if (info == null) {
					info = new SingleIconInfo(image);
					singleIconInfos[i] = info;
					return info;
				}
				if (info.icon == image) {
					return info;
				}
			}
			int index = SingleIconInfo.count % MAX_IMAGE_INFO;
			info = new SingleIconInfo(image);
			singleIconInfos[index] = info;
			return info;
		}
	}

	// reserve 1 bit
	private static int FLAG_SELECTED = MAX_FLAG << 1;

	private static int FLAG_HASFOCUS = MAX_FLAG << 2;

	private static int FLAG_UNDERLINED = MAX_FLAG << 3;

	private static int FLAG_STRIKEDTHROUGH = MAX_FLAG << 4;

	private static int FLAG_WRAP = MAX_FLAG << 5;

	// reserve 3 bits
	private static int FLAG_TEXT_ALIGN = MAX_FLAG << 6;

	private static int FLAG_WRAP_ALIGN = MAX_FLAG << 9;

	private static int FLAG_ICON_ALIGN = MAX_FLAG << 12;

	private static int FLAG_LABEL_ALIGN = MAX_FLAG << 15;

	private static int FLAG_TEXT_PLACEMENT = MAX_FLAG << 18;

	private MapModeConstants mapModeConstants;

	/** the original label's text */
	private String text;

	/** the label's text used in painting after applying required styles */
	private String subStringText;

	/** the size of text */
	private Dimension textSize;	

	private Dimension ellipseTextSize;

	/** the location of text */
	private Point textLocation;

	/** the cached hint used to calculate text size */
	private int cachedPrefSizeHint_width;

	private int cachedPrefSizeHint_height;

	/** the icon location */
	private Point iconLocation;

	private static abstract class IconInfo {
		/**
		 * Gets the icon at the index location.
		 * 
		 * @param i
		 *            the index to retrieve the icon of
		 * @return <code>Image</code> that corresponds to the given index.
		 */
		public abstract Image getIcon(int i);
		
		/**
		 * Gets the icon size of the icon at the given index.
		 * 
		 * @param i
		 * @return the <code>Dimension</code> that is the size of the icon at
		 *         the given index.
		 */
		public abstract Dimension getIconSize(IMapMode mapMode, int i);

		/**
		 * @return the number of icons
		 */
		public abstract int getNumberofIcons();
		
		/**
		 * @return the <code>Dimension</code> that is the total size of all
		 *         the icons.
		 */
		public abstract Dimension getTotalIconSize(IMapMode mapMode);

		public abstract void invalidate();
		
		/**
		 * Sets the icon at the index location.
		 * 
		 * @param icon
		 * @param i
		 */
		public abstract void setIcon(Image icon, int i);
		
		/**
		 * 
		 */
		public abstract int getMaxIcons();

	}	

	private static class SingleIconInfo
		extends IconInfo {	

		static int count;
		
		public static final SingleIconInfo NULL_INFO = new SingleIconInfo(){
			public int getNumberofIcons() {
				return 0;
			}
		};

		final Image icon;

		/** total icon size */
		private Dimension totalIconSize;

		private SingleIconInfo() {
			icon = null;//don't increment count, used only for NULL_INFO
		}

		public SingleIconInfo(Image icon) {
			this.icon = icon;
			++count;
		}

		public final int getMaxIcons() {
			return 1;
		}

		
		public Image getIcon(int i) {
			if (i == 0) {
				return icon;
			} else if (i > 0) {
				return null;
			}
			throw new IndexOutOfBoundsException();
		}

		
		public void setIcon(Image img, int i) {
			throw new UnsupportedOperationException();
		}

		
		public Dimension getIconSize(IMapMode mapMode, int i) {
			if (i == 0) {
				return getTotalIconSize(mapMode);
			}

			throw new IndexOutOfBoundsException();
		}

		
		public int getNumberofIcons() {
			return 1;
		}

		
		public Dimension getTotalIconSize(IMapMode mapMode) {
			if (totalIconSize != null)
				return totalIconSize;

			if (icon != null && !icon.isDisposed()) {
				org.eclipse.swt.graphics.Rectangle imgBounds = icon.getBounds();
				totalIconSize = new Dimension(mapMode.DPtoLP(imgBounds.width),
					mapMode.DPtoLP(imgBounds.height));
			} else {
				totalIconSize = EMPTY_DIMENSION;
			}

			return totalIconSize;
		}

		
		public void invalidate() {
			totalIconSize = null;
		}

	}

	private static class MultiIconInfo
		extends IconInfo {

		/** the label icons */
		private ArrayList icons = new ArrayList(2);

		/** total icon size */
		private Dimension totalIconSize;

		public MultiIconInfo() {
			super();
		}

		public int getMaxIcons() {
			return -1;
		}

		/**
		 * Gets the icon at the index location.
		 * 
		 * @param i
		 *            the index to retrieve the icon of
		 * @return <code>Image</code> that corresponds to the given index.
		 */
		public Image getIcon(int i) {
			if (i >= icons.size())
				return null;

			return (Image) icons.get(i);
		}

		/**
		 * Sets the icon at the index location.
		 * 
		 * @param icon
		 * @param i
		 */
		public void setIcon(Image icon, int i) {
			int size = icons.size();
			if (i >= size) {
				for (int j = size; j < i; j++)
					icons.add(null);
				icons.add(icon);
				icons.trimToSize();
			} else
				icons.set(i, icon);
		}

		/**
		 * Gets the icon size of the icon at the given index.
		 * 
		 * @param i
		 * @return the <code>Dimension</code> that is the size of the icon at
		 *         the given index.
		 */
		public Dimension getIconSize(IMapMode mapMode, int i) {
			Image img = getIcon(i);
			if (img != null && !img.isDisposed()) {
				org.eclipse.swt.graphics.Rectangle imgBounds = img.getBounds();				
				return new Dimension(mapMode.DPtoLP(imgBounds.width), mapMode
					.DPtoLP(imgBounds.height));
			}
			return EMPTY_DIMENSION;
		}

		/**
		 * @return the number of icons
		 */
		public int getNumberofIcons() {
			return icons.size();
		}

		/**
		 * @return the <code>Dimension</code> that is the total size of all
		 *         the icons.
		 */
		public Dimension getTotalIconSize(IMapMode mapMode) {
			if (totalIconSize != null)
				return totalIconSize;
			int iconNum = getNumberofIcons();
			if (iconNum == 0) {
				return totalIconSize = EMPTY_DIMENSION;
			}

			totalIconSize = new Dimension();
			for (int i = 0; i < iconNum; i++) {
				Dimension iconSize = getIconSize(mapMode, i);
				totalIconSize.width += iconSize.width;
				if (iconSize.height > totalIconSize.height)
					totalIconSize.height = iconSize.height;
			}

			return totalIconSize;
		}

		/**
		 * 
		 */
		public void invalidate() {
			totalIconSize = null;
		}
	}

	private IconInfo iconInfo;

	/** the cached hint used to calculate text size */	
	private int cachedTextSizeHint_width;

	private int cachedTextSizeHint_height;
	
	
	
	/**
	 * Construct an empty Label.
	 * 
	 * @since 2.0
	 */
	public OriginalWrapLabel() {
		text = "";//$NON-NLS-1$
		// set defaults
		setAlignmentFlags(CENTER, FLAG_TEXT_ALIGN);
		setAlignmentFlags(CENTER, FLAG_ICON_ALIGN);
		setAlignmentFlags(CENTER, FLAG_LABEL_ALIGN);
		setAlignmentFlags(LEFT, FLAG_WRAP_ALIGN);
		setPlacementFlags(EAST, FLAG_TEXT_PLACEMENT);
	}

	/**
	 * Construct a Label with passed String as its text.
	 * 
	 * @param s the label text
	 * @since 2.0
	 */
	public OriginalWrapLabel(String s) {
		if (s != null) {
			text = s;
		} else {
			text = "";//$NON-NLS-1$
		}
//		setBorder(new LineBorderEx(ColorConstants.red,3));
	}

	/**
	 * Construct a Label with passed Image as its icon.
	 * 
	 * @param i the label image
	 * @since 2.0
	 */
	public OriginalWrapLabel(Image i) {
		text = "";//$NON-NLS-1$
		iconInfo = new SingleIconInfo(i);
	}

	/**
	 * Construct a Label with passed String as text and passed Image as its
	 * icon.
	 * 
	 * @param s the label text
	 * @param i the label image
	 * @since 2.0
	 */
	public OriginalWrapLabel(String s, Image i) {
		if (s != null) {
			text = s;
		} else {
			text = "";//$NON-NLS-1$
		}
		iconInfo = new SingleIconInfo(i);
	}
	
	/**
	 * @return <code>IMapMode</code> used by this figure.
	 *         <code>IMapMode</code> that allows for the coordinate mapping
	 *         from device to logical units.
	 */
	private IMapMode getFigureMapMode() {
		return (IMapMode) getMapModeConstants().mapModeRef.get();
	}

	private MapModeConstants getMapModeConstants() {
		if (mapModeConstants == null) {
			IMapMode mapMode = MapModeUtil.getMapMode(this);
			while (mapMode instanceof IMapModeHolder) {
				mapMode = ((IMapModeHolder) mapMode).getMapMode();
			}
			mapModeConstants = (MapModeConstants) mapModeConstantsMap
				.get(mapMode);
			if (mapModeConstants == null) {
				mapModeConstants = new MapModeConstants(mapMode);
				mapModeConstantsMap.put(mapMode, mapModeConstants);
			}
		}
		return mapModeConstants;
	}

	private void alignOnHeight(Point loc, Dimension size, int alignment) {
		switch (alignment) {
			case TOP:
				loc.y = getInsets().top;
				break;
			case BOTTOM:
				loc.y = bounds.height - size.height - getInsets().bottom;
				break;
			default:
				loc.y = (bounds.height - size.height) / 2;
		}
	}

	private void alignOnWidth(Point loc, Dimension size, int alignment) {
		switch (alignment) {
			case LEFT:
				loc.x = getInsets().left;
				break;
			case RIGHT:
				loc.x = bounds.width - size.width - getInsets().right;
				break;
			default:
				loc.x = (bounds.width - size.width) / 2;
		}
	}

	private void calculateAlignment(Dimension iconSize, int textPlacement) {
		switch (textPlacement) {
			case EAST:
			case WEST:
				alignOnHeight(textLocation, getTextSize(), getTextAlignment());
				alignOnHeight(getIconLocation(), iconSize, getIconAlignment());
				break;
			case NORTH:
			case SOUTH:
				alignOnWidth(textLocation, getSubStringTextSize(),
					getTextAlignment());
				alignOnWidth(getIconLocation(), iconSize, getIconAlignment());
				break;
		}
	}

	/**
	 * Calculates the size of the Label using the passed Dimension as the size
	 * of the Label's text.
	 * 
	 * @param txtSize the precalculated size of the label's text
	 * @return the label's size
	 * @since 2.0
	 */
	protected Dimension calculateLabelSize(Dimension txtSize) {
		Dimension iconSize = getTotalIconSize();
		boolean isEmpty = (iconSize.width == 0 && iconSize.height == 0);
		int len = getText().length();
		if (len == 0 && isEmpty) {
			return new Dimension(txtSize.width, txtSize.height);
		}
		int gap = (len == 0 || isEmpty) ? 0
			: getIconTextGap();
		int placement = getTextPlacement();
		if (placement == WEST || placement == EAST) {
			return new Dimension(iconSize.width + gap + txtSize.width, Math
				.max(iconSize.height, txtSize.height));
		} else {
			return new Dimension(Math.max(iconSize.width, txtSize.width),
				iconSize.height + gap + txtSize.height);
		}
	}

	private void calculateLocations() {
		textLocation = new Point();
		iconLocation = new Point();
		Dimension iconSize = getTotalIconSize();
		int textPlacement = getTextPlacement();
		calculatePlacement(iconSize, textPlacement);
		calculateAlignment(iconSize, textPlacement);
		Rectangle r = getBounds();
		Dimension ps = getPreferredSize(r.width, r.height);
		int w = (r.width - ps.width)
			+ (getTextSize().width - getSubStringTextSize().width);
		int h = r.height - ps.height;
		if (w == 0 && h == 0) {
			return;
		}

		Dimension offset = new Dimension(w, h);
		switch (getLabelAlignment()) {
			case CENTER:
				offset.scale(0.5f);
				break;
			case LEFT:
				offset.scale(0.0f);
				break;
			case RIGHT:
				offset.scale(1.0f);
				break;
			case TOP:
				offset.height = 0;
				offset.scale(0.5f);
				break;
			case BOTTOM:
				offset.height = offset.height * 2;
				offset.scale(0.5f);
				break;
			default:
				offset.scale(0.5f);
				break;
		}

		switch (textPlacement) {
			case EAST:
			case WEST:
				offset.height = 0;
				break;
			case NORTH:
			case SOUTH:
				offset.width = 0;
				break;
		}

		textLocation.translate(offset);
		iconLocation.translate(offset);
	}

	private void calculatePlacement(Dimension iconSize, int textPlacement) {
		int gap = (getText().length() == 0 || (iconSize.width == 0 && iconSize.height == 0)) ? 0
			: getIconTextGap();
		Insets insets = getInsets();
		switch (textPlacement) {
			case EAST:
				iconLocation.x = insets.left;
				textLocation.x = iconSize.width + gap + insets.left;
				break;
			case WEST:
				textLocation.x = insets.left;
				iconLocation.x = getSubStringTextSize().width + gap
					+ insets.left;
				break;
			case NORTH:
				textLocation.y = insets.top;
				iconLocation.y = getTextSize().height + gap + insets.top;
				break;
			case SOUTH:
				textLocation.y = iconSize.height + gap + insets.top;
				iconLocation.y = insets.top;
		}
	}
	/**
	 * Calculates the size of the Label's text size. The text size calculated
	 * takes into consideration if the Label's text is currently truncated. If
	 * text size without considering current truncation is desired, use
	 * {@link #calculateTextSize(int, int)}.
	 * 
	 * @return the size of the label's text, taking into account truncation
	 * @since 2.0
	 */
	protected Dimension calculateSubStringTextSize() {
		Font f = getFont();
		return getTextExtents(getSubStringText(), f, getFigureMapMode().DPtoLP(FigureUtilities.getFontMetrics(f).getHeight())); 
	}

	/**
	 * Calculates and returns the size of the Label's text. Note that this
	 * Dimension is calculated using the Label's full text, regardless of
	 * whether or not its text is currently truncated. If text size considering
	 * current truncation is desired, use {@link #calculateSubStringTextSize()}.
	 * 
	 * @param wHint a width hint
	 * @param hHint a height hint
	 * @return the size of the label's text, ignoring truncation
	 * @since 2.0
	 */
	protected Dimension calculateTextSize(int wHint, int hHint) {
		Font f = getFont();
		return getTextExtents(getWrappedText(wHint, hHint), f,getFigureMapMode().DPtoLP(FigureUtilities.getFontMetrics(f).getHeight()));
	}

	private void clearLocations() {
		iconLocation = textLocation = null;
	}

	/**
	 * Returns the Label's icon.
	 * 
	 * @return the label icon
	 * @since 2.0
	 */
	public Image getIcon() {
		return getIcon(0);
	}

	/**
	 * Gets the label's icon at the given index
	 * 
	 * @param index The icon index
	 * @return the <code>Image</code> that is the icon for the given index.
	 */
	public Image getIcon(int index) {
		if (iconInfo == null)
			return null;
		return iconInfo.getIcon(index);
	}

	/**
	 * Determines if there is any icons by checking if icon size is zeros.
	 * 
	 * @return true if icons are present, false otherwise 
	 */
	protected boolean hasIcons() {
		return (getNumberofIcons() > 0);
	}

	/**
	 * Returns the current alignment of the Label's icon. The default is
	 * {@link PositionConstants#CENTER}.
	 * 
	 * @return the icon alignment
	 * @since 2.0
	 */
	public int getIconAlignment() {
		return getAlignment(FLAG_ICON_ALIGN);
	}

	/**
	 * Returns the bounds of the Label's icon.
	 * 
	 * @return the icon's bounds
	 * @since 2.0
	 */
	public Rectangle getIconBounds() {
		return new Rectangle(getBounds().getLocation().translate(
			getIconLocation()), getTotalIconSize());
	}

	/**
	 * Returns the location of the Label's icon relative to the Label.
	 * 
	 * @return the icon's location
	 * @since 2.0
	 */
	protected Point getIconLocation() {
		if (iconLocation == null)
			calculateLocations();
		return iconLocation;
	}

	/**
	 * Returns the gap in pixels between the Label's icon and its text.
	 * 
	 * @return the gap
	 * @since 2.0
	 */
	public int getIconTextGap() {
		return getMapModeConstants().nDPtoLP_3;
	}

	/**
	 * @see IFigure#getMinimumSize(int, int)
	 */
	public Dimension getMinimumSize(int w, int h) {
		if (minSize != null)
			return minSize;
		minSize = new Dimension();
		LayoutManager layoutManager = getLayoutManager();
		if (layoutManager != null)
			minSize.setSize(layoutManager.getMinimumSize(this, w, h));
		Font f = getFont();
		Dimension d = getEllipseTextSize().getIntersected(
			getTextExtents(getText(), f, getFigureMapMode().DPtoLP(FigureUtilities.getFontMetrics(f).getHeight())));		
		
		Dimension labelSize = calculateLabelSize(d);
		Insets insets = getInsets();
		labelSize.expand(insets.getWidth(), insets.getHeight());
		minSize.union(labelSize);
		return minSize;
	}

	/* 
	 * (non-Javadoc)
	 * @see org.eclipse.draw2d.IFigure#getPreferredSize(int, int)
	 */
	public Dimension getPreferredSize(int wHint, int hHint) {
		if (prefSize == null || wHint != cachedPrefSizeHint_width || hHint != cachedPrefSizeHint_height) {
			prefSize = calculateLabelSize(getTextSize(wHint, hHint));
			Insets insets = getInsets();
			prefSize.expand(insets.getWidth(), insets.getHeight());
			LayoutManager layoutManager = getLayoutManager();
			if (layoutManager != null) {
				prefSize.union(layoutManager.getPreferredSize(this, wHint,
					hHint));
			}
			prefSize.union(getMinimumSize(wHint, hHint));
			cachedPrefSizeHint_width = wHint;
			cachedPrefSizeHint_height = hHint;
		}
		return prefSize;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.draw2d.IFigure#getMaximumSize()
	 */
	public Dimension getMaximumSize() {
		// this assumes that getPreferredSize(wHint, hHint) is called before
		return prefSize;   
	}

	/**
	 * Calculates the amount of the Label's current text will fit in the Label,
	 * including an elipsis "..." if truncation is required.
	 * 
	 * @return the substring
	 * @since 2.0
	 */
	public String getSubStringText() {
		if (subStringText != null)
			return subStringText;
		
		String theText = getText();
		int textLen = theText.length();
		if (textLen == 0) {
			return subStringText = "";//$NON-NLS-1$;;
		}
		Dimension size = getSize();
		Dimension shrink = getPreferredSize(size.width, size.height).getShrinked(size);
		Dimension effectiveSize = getTextSize().getExpanded(-shrink.width, -shrink.height);
		
		if (effectiveSize.height == 0) {
			return subStringText = "";//$NON-NLS-1$;
		}
		
		Font f = getFont();
		FontMetrics metrics = FigureUtilities.getFontMetrics(f);
		IMapMode mm = getFigureMapMode();
		int fontHeight = mm.DPtoLP(metrics.getHeight());
		int charAverageWidth = mm.DPtoLP(metrics.getAverageCharWidth());
		int maxLines = (int) (effectiveSize.height / (double) fontHeight);
		if (maxLines == 0) {
			return subStringText = "";//$NON-NLS-1$
		}

		StringBuffer accumlatedText = new StringBuffer();
		StringBuffer remainingText = new StringBuffer(theText);
		
		int effectiveSizeWidth = effectiveSize.width;
		int widthHint = Math.max(effectiveSizeWidth
			- getEllipseTextSize().width, 0);
		int i = 0, j = 0;
		while (remainingText.length() > 0 && j++ < maxLines) {
			i = getLineWrapPosition(remainingText.toString(), f, effectiveSizeWidth, fontHeight);

			if (accumlatedText.length() > 0)
				accumlatedText.append('\n');

			if (i == 0 || (remainingText.length() > i && j == maxLines)) {				
				i = getLargestSubstringConfinedTo(remainingText.toString(), f, widthHint, fontHeight, charAverageWidth);
				accumlatedText.append(remainingText.substring(0, i));
				accumlatedText.append(getEllipse());
			} else
				accumlatedText.append(remainingText.substring(0, i));
			remainingText.delete(0, i);
		}
		return subStringText = accumlatedText.toString();
	}
	
	
	
	
	/**
	 * Creates an equivalent text to that of the label's but with "\n"(s)
	 * inserted at the wrapping positions. This method assumes unlimited
	 * bounding box and is used by <code>calculateTextSize()</code> to
	 * calculate the perfect size of the text with wrapping
	 * 
	 * @return the wrapped text
	 */	
	private String getWrappedText(int wHint, int hHint) {
		String theText = getText();		
		if (wHint == -1 || theText.length() == 0 || !isTextWrapped())
			return theText;

		Dimension iconSize = getTotalIconSize();
		if (!(iconSize.width == 0 && iconSize.height == 0)) {
			switch(getTextPlacement()) {
				case EAST:
				case WEST:
					wHint -= iconSize.width + getIconTextGap();
					break;
				case NORTH:
				case SOUTH:
					if (hHint != -1)
						hHint -= iconSize.height + getIconTextGap();
					break;
			}
		}
		
		
		if ((hHint == 0)||(wHint == 0)) {
			return "";//$NON-NLS-1$;
		}
		
		Font f = getFont();
		int fontHeight = getFigureMapMode().DPtoLP(FigureUtilities.getFontMetrics(f).getHeight());
		int maxLines = Integer.MAX_VALUE;
		if (hHint != -1) {			
			maxLines = (int) (hHint / (double) fontHeight);
			if (maxLines == 0) {
				return "";//$NON-NLS-1$;;
			}
		}	
		
		StringBuffer accumlatedText = new StringBuffer();
		StringBuffer remainingText = new StringBuffer(theText);
		int i = 0, j = 0;

		while (remainingText.length() > 0 && j++  < maxLines) {
			if ((i = getLineWrapPosition(remainingText.toString(), f, wHint, fontHeight)) == 0)
				break;

			if (accumlatedText.length() > 0)
				accumlatedText.append('\n');
			accumlatedText.append(remainingText.substring(0, i));
			remainingText.delete(0, i);
		}
		return accumlatedText.toString();
	}

	/**
	 * Returns the size of the Label's current text. If the text is currently
	 * truncated, the truncated text with its ellipsis is used to calculate the
	 * size.
	 * 
	 * @return the size of this label's text, taking into account truncation
	 * @since 2.0
	 */
	protected Dimension getSubStringTextSize() {
		return calculateSubStringTextSize();
	}
	
	/**
	 * Returns the size of the String constant "..." the ellipse based on
	 * the currently used Map mode
	 * size.
	 * 
	 * @return the size of ellipse text
	 * 
	 */
	private Dimension getEllipseTextSize() {
		if (ellipseTextSize == null) {
			ellipseTextSize = getMapModeConstants().getEllipseTextSize(
				getFont());
		}
		return ellipseTextSize;
	}

	/**
	 * Returns the text of the label. Note that this is the complete text of the
	 * label, regardless of whether it is currently being truncated. Call
	 * {@link #getSubStringText()}to return the label's current text contents
	 * with truncation considered.
	 * 
	 * @return the complete text of this label
	 * @since 2.0
	 */
	public String getText() {
		return text;
	}

	/**
	 * Returns the current alignment of the Label's text. The default text
	 * alignment is {@link PositionConstants#CENTER}.
	 * 
	 * @return the text alignment
	 */
	public int getTextAlignment() {
		return getAlignment(FLAG_TEXT_ALIGN);
	}

	/**
	 * Returns the current alignment of the entire Label. The default label
	 * alignment is {@link PositionConstants#LEFT}.
	 * 
	 * @return the label alignment
	 */
	int getLabelAlignment() {
		return getAlignment(FLAG_LABEL_ALIGN);
	}
	
	/**
	 * Returns the bounds of the label's text. Note that the bounds are
	 * calculated using the label's complete text regardless of whether the
	 * label's text is currently truncated.
	 * 
	 * @return the bounds of this label's complete text
	 * @since 2.0
	 */
	public Rectangle getTextBounds() {
		return new Rectangle(getBounds().getLocation().translate(
			getTextLocation()), getTextSize());
	}

	/**
	 * Returns the location of the label's text relative to the label.
	 * 
	 * @return the text location
	 * @since 2.0
	 */
	protected Point getTextLocation() {
		if (textLocation != null)
			return textLocation;
		calculateLocations();
		return textLocation;
	}

	/**
	 * Returns the current placement of the label's text relative to its icon.
	 * The default text placement is {@link PositionConstants#EAST}.
	 * 
	 * @return the text placement
	 * @since 2.0
	 */
	public int getTextPlacement() {
		return getPlacement(FLAG_TEXT_PLACEMENT);
	}

	/**
	 * Returns the size of the label's complete text. Note that the text used to
	 * make this calculation is the label's full text, regardless of whether the
	 * label's text is currently being truncated and is displaying an ellipsis.
	 * If the size considering current truncation is desired, call
	 * {@link #getSubStringTextSize()}.
	 * 
	 * @param wHint a width hint
	 * @param hHint a height hint
	 * @return the size of this label's complete text
	 * @since 2.0
	 */
	protected Dimension getTextSize(int wHint, int hHint) {
		if (textSize == null || wHint != cachedTextSizeHint_width || hHint != cachedTextSizeHint_height) {
			textSize = calculateTextSize(wHint, hHint);
			cachedTextSizeHint_width = wHint;
			cachedTextSizeHint_height= hHint;
		}
		return textSize;
	}

	/**
	 * Gets the text size given the current size as a width hint
	 */
	private final Dimension getTextSize() {		
		Rectangle r = getBounds();
		return getTextSize(r.width, r.height);		
	}
	
	/**
	 * @see IFigure#invalidate()
	 */
	public void invalidate() {
		prefSize = null;
		minSize = null;
		clearLocations();
		ellipseTextSize = null;
		textSize = null;
		subStringText = null;
		if (iconInfo != null)
			iconInfo.invalidate();
		super.invalidate();
	}

	/**
	 * Returns <code>true</code> if the label's text is currently truncated
	 * and is displaying an ellipsis, <code>false</code> otherwise.
	 * 
	 * @return <code>true</code> if the label's text is truncated
	 * @since 2.0
	 */
	public boolean isTextTruncated() {
		return !getSubStringTextSize().equals(getTextSize());
	}

	/**
	 * @see org.eclipse.draw2d.Figure#paintFigure(org.eclipse.draw2d.Graphics)
	 */
	public void paintFigure(Graphics graphics) {
		if (isSelected()) {
			graphics.pushState();
			graphics.setBackgroundColor(ColorConstants.menuBackgroundSelected);
			graphics.fillRectangle(getSelectionRectangle());
			graphics.popState();
			graphics.setForegroundColor(ColorConstants.white);
		}
		if (hasFocus()) {
			graphics.pushState();
			graphics.setXORMode(true);
			graphics.setForegroundColor(ColorConstants.menuBackgroundSelected);
			graphics.setBackgroundColor(ColorConstants.white);
			graphics.drawFocus(getSelectionRectangle().resize(-1, -1));
			graphics.popState();
		}
		if (isOpaque())
			super.paintFigure(graphics);

		Rectangle figBounds = getBounds();
		graphics.translate(figBounds.x, figBounds.y);
		if (hasIcons())
			paintIcons(graphics);

		String subString = getSubStringText();
		if (subString.length() > 0) {
			if (!isEnabled()) {
				graphics.translate(1, 1);
				graphics.setForegroundColor(ColorConstants.buttonLightest);
				paintText(graphics, subString);
				graphics.translate(-1, -1);
				graphics.setForegroundColor(ColorConstants.buttonDarker);
			} else {
				paintText(graphics, subString);
			}
		}
		graphics.translate(-figBounds.x, -figBounds.y);
	}

	/**
	 * Paints the text and optioanally underlines it
	 * 
	 * @param graphics The graphics context
	 * @param subString The string to draw
	 */	
	private void paintText(Graphics graphics, String subString) {		
		StringTokenizer tokenizer = new StringTokenizer(subString, "\n"); //$NON-NLS-1$
		Font f = getFont();
		FontMetrics fontMetrics = FigureUtilities.getFontMetrics(f);
		int fontHeight = getFigureMapMode().DPtoLP(fontMetrics.getHeight());
		int fontHeightHalf = fontHeight / 2;
		int textWidth = getTextExtents(subString, f, fontHeight).width;
		Point p = getTextLocation();
		int y = p.y;
		int x = p.x;
		final int wrapAlignment = getTextWrapAlignment();
		boolean isUnderlined = isTextUnderlined();
		boolean isStrikedThrough = isTextStrikedThrough();
		Rectangle clipRect = new Rectangle();
		graphics.getClip(clipRect);
		int clipRectTopRight_x = clipRect.getTopRight().x;
		// If the font's leading area is 0 then we need to add an offset to
		// avoid truncating at the top (e.g. Korean fonts)
		if (0 == fontMetrics.getLeading()) {
			y +=  getMapModeConstants().nDPtoLP_2; // 2 is the leading area for default English			
		}				

		while (tokenizer.hasMoreTokens()) {
            x = p.x;
			String token = tokenizer.nextToken();
			int tokenWidth = getTextExtents(token, f, fontHeight).width;
			
			switch (wrapAlignment) {
				case CENTER:
					x += (textWidth - tokenWidth) / 2;
					break;
				case RIGHT:
					x += textWidth - tokenWidth;
					break;
			}
			
			// increase the clipping rectangle by a small amount to account for font overhang
			// from italic / irregular characters etc.
			
			
			if (tokenWidth + x <= clipRectTopRight_x) {
				Rectangle newClipRect = new Rectangle(clipRect);
				newClipRect.width += (tokenWidth / token.length()) / 2;
				graphics.setClip(newClipRect);
			}
				
			graphics.drawText(token, x, y);
			graphics.setClip(clipRect);
			
			y += fontHeight;

			if (isUnderlined)
				graphics.drawLine(x, y - 1, x + tokenWidth, y - 1);
			if (isStrikedThrough)
				graphics.drawLine(x, y - fontHeightHalf + 1, x + tokenWidth, y
					- fontHeightHalf + 1);
		}
	}

	/**
	 * Paints the icon(s)
	 * 
	 * @param graphics The graphics context
	 */
	private void paintIcons(Graphics graphics) {
		Point p = Point.SINGLETON;
		p.setLocation(getIconLocation());

		int num = getNumberofIcons();
		for (int i = 0; i < num; i++) {
			Image icon = getIcon(i); 
			if (icon != null) {
				graphics.drawImage(icon, p);
				p.x += getIconSize(i).width;
			}
		}
	}

	/**
	 * Sets the label's icon to the passed image.
	 * 
	 * @param image the new label image
	 * @since 2.0
	 */
	public void setIcon(Image image) {
		setIcon(image, 0);
	}

	/**
	 * Sets the label's icon at given index
	 * 
	 * @param image The icon image or null to remove the icon
	 * @param index The icon index
	 */
	public void setIcon(Image image, int index) {
		if (iconInfo == null) {
			if (index == 0) {
				iconInfo = getMapModeConstants().getSingleIconInfo(image);
			} else {
				iconInfo = new MultiIconInfo();
				iconInfo.setIcon(image, index);
			}
			revalidate();
			repaint();// Call repaint, in case the image dimensions are the same.           
		} else if (iconInfo.getIcon(index) != image) {
			if (iconInfo.getMaxIcons() == 1) {
				if (index == 0) {
					iconInfo = getMapModeConstants().getSingleIconInfo(image);
					revalidate();
					repaint();// Call repaint, in case the image dimensions are the same.
					return;
				}
				IconInfo oldIconInfo = iconInfo;
				iconInfo = new MultiIconInfo();
				iconInfo.setIcon(oldIconInfo.getIcon(0), 0);
			}
			iconInfo.setIcon(image, index);
			revalidate();
			repaint();// Call repaint, in case the image dimensions are the same.
		}	
	}


	/**
	 * Sets the icon alignment relative to the .abel's alignment to the passed
	 * value. The default is {@link PositionConstants#CENTER}. Other possible
	 * values are {@link PositionConstants#TOP},
	 * {@link PositionConstants#BOTTOM},{@link PositionConstants#LEFT}and
	 * {@link PositionConstants#RIGHT}.
	 * 
	 * @param align the icon alignment
	 * @since 2.0
	 */
	public void setIconAlignment(int align) {
		if (getIconAlignment() == align)
			return;
		setAlignmentFlags(align, FLAG_ICON_ALIGN);
		clearLocations();
		repaint();
	}

	/**
	 * getIconSize
	 * @param index of icon to retrieve size of.
	 * @return Dimension representing the icon size.
	 */
	protected Dimension getIconSize(int index) {
		if (iconInfo == null)
			return EMPTY_DIMENSION;
		return iconInfo.getIconSize(getFigureMapMode(), index);
	}
	
	/**
	 * getIconNumber
	 * @return int number of icons in the wrap label
	 */
	protected int getNumberofIcons() {
		if (iconInfo == null)
			return 0;
		return iconInfo.getNumberofIcons();
	}
	
	/**
	 * getTotalIconSize
	 * Calculates the total union of icon sizes
	 * @return Dimension that is the union of icon sizes
	 */
	protected Dimension getTotalIconSize() {
		if (iconInfo == null)
			return EMPTY_DIMENSION;
		return iconInfo.getTotalIconSize(getFigureMapMode());
	}

	/**
	 * Sets the Label's alignment to the passed value. The default is
	 * {@link PositionConstants#CENTER}. Other possible values are
	 * {@link PositionConstants#TOP},{@link PositionConstants#BOTTOM},
	 * {@link PositionConstants#LEFT}and {@link PositionConstants#RIGHT}.
	 * 
	 * @param align label alignment
	 */
	public void setLabelAlignment(int align) {
		if (getLabelAlignment() == align)
			return;
		setAlignmentFlags(align, FLAG_LABEL_ALIGN);
		clearLocations();
		repaint();
	}

	/**
	 * Return the ellipse string.
	 * 
	 * @return the <code>String</code> that represents the fact that the
	 * text has been truncated and that more text is available but hidden. 
	 * Usually this is represented by "...".
	 */
	protected String getEllipse() {
		return _ellipse;
	}
	
	/**
	 * Sets the label's text.
	 * 
	 * @param s the new label text
	 * @since 2.0
	 */
	public void setText(String s) {
		//"text" will never be null.
		if (s == null)
			s = "";//$NON-NLS-1$
		if (text.equals(s))
			return;
		text = s;
		revalidate();
		repaint(); //If the new text does not cause a new size, we still need
		// to paint.
	}

	/**
	 * Sets the text alignment of the Label relative to the label alignment. The
	 * default is {@link PositionConstants#CENTER}. Other possible values are
	 * {@link PositionConstants#TOP},{@link PositionConstants#BOTTOM},
	 * {@link PositionConstants#LEFT}and {@link PositionConstants#RIGHT}.
	 * 
	 * @param align the text alignment
	 * @since 2.0
	 */
	public void setTextAlignment(int align) {
		if (getTextAlignment() == align)
			return;
		setAlignmentFlags(align, FLAG_TEXT_ALIGN);
		clearLocations();
		repaint();
	}

	/**
	 * Sets the text placement of the label relative to its icon. The default is
	 * {@link PositionConstants#EAST}. Other possible values are
	 * {@link PositionConstants#NORTH},{@link PositionConstants#SOUTH}and
	 * {@link PositionConstants#WEST}.
	 * 
	 * @param where the text placement
	 * @since 2.0
	 */
	public void setTextPlacement(int where) {
		if (getTextPlacement() == where)
			return;
		setPlacementFlags(where, FLAG_TEXT_PLACEMENT);
		revalidate();
		repaint();
	}

	/**
	 * Sets whether the label text should be underlined
	 * 
	 * @param b Wether the label text should be underlined
	 */
	public void setTextUnderline(boolean b) {
		if (isTextUnderlined() == b)
			return;
		setFlag(FLAG_UNDERLINED, b);
		repaint();
	}

	/**
	 * @return whether the label text is underlined
	 */
	public boolean isTextUnderlined() {
		return (flags & FLAG_UNDERLINED) != 0;
	}
	
	/**
	 * Sets whether the label text should be striked-through
	 * 
	 * @param b Wether the label text should be stricked-through
	 */
	public void setTextStrikeThrough(boolean b) {
		if (isTextStrikedThrough() == b)
			return;
		setFlag(FLAG_STRIKEDTHROUGH, b);
		repaint();
	}

	/**
	 * @return wether the label text is stricked-through
	 */
	public boolean isTextStrikedThrough() {
		return (flags & FLAG_STRIKEDTHROUGH) != 0;
	}

	/**
	 * Sets whether the label text should wrap
	 * 
	 * @param b whether the label text should wrap
	 */
	public void setTextWrap(boolean b) {
		if (isTextWrapped() == b)
			return;
		setFlag(FLAG_WRAP, b);
		revalidate();
		repaint();
	}

	/**
	 * @return wether the label text wrap is on
	 */
	public boolean isTextWrapped() {
		return (flags & FLAG_WRAP) != 0;
	}

	/**
	 * Sets the wrapping width of the label text. This is only valid if text
	 * wrapping is turned on
	 * 
	 * @param i The label text wrapping width
	 */
	public void setTextWrapWidth(int i) {
		/*
		 * if (this.wrapWidth == i) return; this.wrapWidth = i; revalidate();
		 * repaint();
		 */
	}

	/**
	 * Sets the wrapping width of the label text. This is only valid if text
	 * wrapping is turned on
	 * 
	 * @param i The label text wrapping width
	 */
	public void setTextWrapAlignment(int i) {
		if (getTextWrapAlignment() == i)
			return;
		
		setAlignmentFlags(i, FLAG_WRAP_ALIGN);
		repaint();
	}

	/**
	 * @return the label text wrapping width
	 */
	public int getTextWrapAlignment() {
		return getAlignment(FLAG_WRAP_ALIGN);
	}
	
	/**
	 * setPlacementFlags
	 * @param align 
	 * @param flagOffset
	 */
	private void setPlacementFlags(int align, int flagOffset) {
		flags &= ~(0x7 * flagOffset);
		switch (align) {
			case EAST:
				flags |= 0x1 * flagOffset;
				break;
			case WEST:
				flags |= 0x2 * flagOffset;
				break;
			case NORTH:
				flags |= 0x3 * flagOffset;
				break;
			case SOUTH:
				flags |= 0x4 * flagOffset;
				break;
		}
	}

	/**
	 * getPlacement
	 * 
	 * @param flagOffset
	 * @return PositionConstant representing the placement
	 */
	private int getPlacement(int flagOffset) {
		int wrapValue = flags & (0x7 * flagOffset);
		if (wrapValue == 0x1 * flagOffset)
			return EAST;
		else if (wrapValue == 0x2 * flagOffset)
			return WEST;
		else if (wrapValue == 0x3 * flagOffset)
			return NORTH;
		else if (wrapValue == 0x4 * flagOffset)
			return SOUTH;
		
		return EAST;
	}
	
	/**
	 * setAlignmentFlags
	 * @param align 
	 * @param flagOffset
	 */
	private void setAlignmentFlags(int align, int flagOffset) {
		flags &= ~(0x7 * flagOffset);
		switch (align) {
			case CENTER:
				flags |= 0x1 * flagOffset;
				break;
			case TOP:
				flags |= 0x2 * flagOffset;
				break;
			case LEFT:
				flags |= 0x3 * flagOffset;
				break;
			case RIGHT:
				flags |= 0x4 * flagOffset;
				break;
			case BOTTOM:
				flags |= 0x5 * flagOffset;
				break;
		}
	}

	/**
	 * Retrieves the alignment value from the flags member.
	 * 
	 * @param flagOffset that is the bitwise value representing the offset.
	 * @return PositionConstant representing the alignment
	 */
	private int getAlignment(int flagOffset) {
		int wrapValue = flags & (0x7 * flagOffset);
		if (wrapValue == 0x1 * flagOffset)
			return CENTER;
		else if (wrapValue == 0x2 * flagOffset)
			return TOP;
		else if (wrapValue == 0x3 * flagOffset)
			return LEFT;
		else if (wrapValue == 0x4 * flagOffset)
			return RIGHT;
		else if (wrapValue == 0x5 * flagOffset)
			return BOTTOM;
		
		return CENTER;
	}
	

	/**
	 * Sets the selection state of this label
	 * 
	 * @param b true will cause the label to appear selected
	 */
	public void setSelected(boolean b) {
		if (isSelected() == b)
			return;
		setFlag(FLAG_SELECTED, b);
		repaint();
	}

	/**
	 * @return the selection state of this label
	 */
	public boolean isSelected() {
		return (flags & FLAG_SELECTED) != 0;
	}

	/**
	 * Sets the focus state of this label
	 * 
	 * @param b true will cause a focus rectangle to be drawn around the text
	 *            of the Label
	 */
	public void setFocus(boolean b) {
		if (hasFocus() == b)
			return;
		setFlag(FLAG_HASFOCUS, b);
		repaint();
	}

	/**
	 * @return the focus state of this label
	 */
	public boolean hasFocus() {
		return (flags & FLAG_HASFOCUS) != 0;
	}

	/**
	 * Returns the bounds of the text selection
	 * 
	 * @return The bounds of the text selection
	 */
	private Rectangle getSelectionRectangle() {
		Rectangle figBounds = getTextBounds();
		int expansion = getMapModeConstants().nDPtoLP_2;
		figBounds.resize(expansion, expansion);
		translateToParent(figBounds);
		figBounds.intersect(getBounds());
		return figBounds;
	}

	/**
	 * returns the position of last character within the supplied text that will
	 * fit within the supplied width.
	 * 
	 * @param s a text string
	 * @param f font used to draw the text string
	 * @param w width in pixles.
	 * @param fontHeight int <b>mapped already to logical units</b>.
	 */
	private int getLineWrapPosition(String s, Font f, int w, int fontHeight) {
		if (getTextExtents(s, f, fontHeight).width <= w) {
			return s.length();
		}
		// create an iterator for line breaking positions
		BreakIterator iter = BreakIterator.getLineInstance();
		iter.setText(s);
		int start = iter.first();
		int end = iter.next();

		// if the first line segment does not fit in the width,
		// determine the position within it where we need to cut
		if (getTextExtents(s.substring(start, end), f, fontHeight).width > w) {
			iter = BreakIterator.getCharacterInstance();
			iter.setText(s);
			start = iter.first();
		}

		// keep iterating as long as width permits
		do
			end = iter.next();
		while (end != BreakIterator.DONE
			&& getTextExtents(s.substring(start, end), f, fontHeight).width <= w);
		return (end == BreakIterator.DONE) ? iter.last()
			: iter.previous();
	}	

	/**
	 * Returns the largest substring of <i>s </i> in Font <i>f </i> that can be
	 * confined to the number of pixels in <i>availableWidth <i>.
	 * 
	 * @param s the original string
	 * @param f the font
	 * @param w the available width
	 * @param fontHeight int <b>mapped already to logical units</b>.
	 * @param charAverageWidth int <b>mapped already to logical units</b>.
	 * @return the largest substring that fits in the given width
	 * @since 2.0
	 */
	private int getLargestSubstringConfinedTo(String s, Font f, int w, int fontHeight, int charAverageWidth) {		
		float avg = charAverageWidth;
		int min = 0;
		int max = s.length() + 1;

		//The size of the current guess
		int guess = 0, guessSize = 0;
		while ((max - min) > 1) {
			//Pick a new guess size
			//	New guess is the last guess plus the missing width in pixels
			//	divided by the average character size in pixels
			guess = guess + (int) ((w - guessSize) / avg);

			if (guess >= max)
				guess = max - 1;
			if (guess <= min)
				guess = min + 1;

			//Measure the current guess
			guessSize = getTextExtents(s.substring(0, guess), f, fontHeight).width;

			if (guessSize < w)
				//We did not use the available width
				min = guess;
			else
				//We exceeded the available width
				max = guess;
		}
		return min;
	}

	/**
	 * Gets the tex extent scaled to the mapping mode
	 */
	private Dimension getTextExtents(String s, Font f, int fontHeight) {
		if (s.length() == 0) {
			return getMapModeConstants().dimension_nDPtoLP_0;
		} else {
			// height should be set using the font height and the number of
			// lines in the string			
			Dimension d = FigureUtilities.getTextExtents(s, f);
			IMapMode mapMode = getFigureMapMode();
			d.width = mapMode.DPtoLP(d.width);
			d.height = fontHeight * new StringTokenizer(s, "\n").countTokens();//$NON-NLS-1$
			return d;			
		}
	}

    
	
}
